﻿// Copyright 2012 Stuart Turner
// Licensed under the Apache License, Version 2.0 [http://www.apache.org/licenses/LICENSE-2.0]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

using Complete.Data.Linq;
using Complete.Data.Linq.Expressions;
using Complete.Data.Support;

namespace Complete.Data.Internal
{
	internal class QueryBinder : CompleteExpressionVisitor
	{
		private Stack<FromExpression> fromExpressions = new Stack<FromExpression>();
		private int tableCount = 1;
		private INameMapper mapper;

		private QueryBinder(INameMapper mapper)
		{
			this.mapper = mapper;
		}

		private string GetNextAlias()
		{
			return "t" + (tableCount++);
		}

		#region Expression Visitor Functions
		protected override Expression VisitConstant(ConstantExpression c)
		{
			switch (Type.GetTypeCode(c.Type))
			{
				case TypeCode.Object:
					if (!c.Type.IsGenericType)
						goto default;

					var definition = c.Value as ITableDefinition;
					if (definition == null)
						goto default;

					var table = BuildTable(definition);
					var from = CompleteExpression.From(table);

					return CompleteExpression.Select(
						GetNextAlias(),
						ColumnReplacer.ReplaceColumns(table.Projection) as LambdaExpression,
						from,
						orderBy: null,
						isProjection: false);

				default:
					return base.VisitConstant(c);
			}
		}

		protected override MemberAssignment VisitMemberAssignment(MemberAssignment assignment)
		{
			return Expression.Bind(assignment.Member, CompleteExpression.Column(this.Visit(assignment.Expression), assignment.Member.Name));
		}

		protected override Expression VisitMemberAccess(MemberExpression m)
		{
			if (fromExpressions.Count == 0)
				throw new InvalidOperationException("Not sure how we got here, but ok?");

			do
			{
				foreach (var f in fromExpressions)
				{
					var c = f.FindColumn(m);
					if (c != null)
						return c.Clone();

					var x = f.FindObject(m);
					if (x != null)
					{
						var lambda = x as LambdaExpression;
						if (lambda != null)
							x = lambda.Body;
						return ColumnReplacer.ReplaceColumns(x);
					}
				}
			} while (BuildRelationships(m));

			throw new InvalidOperationException("Could not find column in source table.");
		}

		protected override NewExpression VisitNew(NewExpression nex)
		{
			var args = nex.Arguments;
			if (!args.Any())
				return nex;

			var @params = nex.Constructor.GetParameters();
			return Expression.New(
				nex.Constructor,
				nex.Arguments
					.Select(a => this.Visit(a))
					.Select((a, i) =>
					{
						if (a.Type.IsComplexType()) return a;
						return CompleteExpression.Column(a, @params[i].Name);
					}),
				nex.Members);
		}

		protected override Expression VisitParameter(ParameterExpression p)
		{
			if (fromExpressions.Count == 0)
				throw new InvalidOperationException("Not sure how we got here, but ok?");
			foreach (var f in fromExpressions)
			{
				var x = f.FindObject(p);
				if (x != null)
					return ColumnReplacer.ReplaceColumns(x);
			}

			throw new InvalidOperationException("Could not find column in source table.");
		}

		protected override Expression VisitMethodCall(MethodCallExpression m)
		{
			var method = m.Method;
			if (method.DeclaringType == typeof(Queryable) || method.DeclaringType == typeof(Enumerable))
			{
				switch (method.Name)
				{
					case "Where":
						return VisitWhereMethod(m);

					case "Select":
						return VisitSelectMethod(m);

					case "SelectMany":
						return VisitSelectManyMethod(m);

					case "Join":
						return VisitJoinMethod(m);

					case "DefaultIfEmpty":
						return VisitDefaultIfEmptyMethod(m);

					case "Take":
						return VisitTakeMethod(m);

					case "OrderBy":
					case "OrderByDescending":
						return VisitOrderByMethod(m);

					case "ThenBy":
					case "ThenByDescending":
						return VisitThenByMethod(m);

					default:
						throw new NotSupportedException("Haven't processed this function yet.");
				}
			}

			return base.VisitMethodCall(m);
		}

		protected override Expression VisitBinary(BinaryExpression b)
		{
			if ((b.NodeType == ExpressionType.Equal || b.NodeType == ExpressionType.NotEqual) &&
				(b.Left.Type.IsComplexType()))
			{
				var leftColumns = DistinctColumnExtractor.GetDistinctColumns(mapper, b.Left);
				var rightColumns = DistinctColumnExtractor.GetDistinctColumns(mapper, b.Right);

				if (leftColumns.Count() != rightColumns.Count())
					throw new InvalidOperationException("Binary Comparison failed.  Left and right are same type, so something weird happened here.");

				return
					leftColumns
						.Zip(rightColumns, (l, r) => Expression.MakeBinary(b.NodeType, l, r))
						.Aggregate((l, r) => Expression.MakeBinary(
							b.NodeType == ExpressionType.Equal ?
								ExpressionType.AndAlso :
								ExpressionType.OrElse,
							l,
							r));
			}
			else
				return base.VisitBinary(b);
		}
		#endregion

		#region Support Functions
		protected virtual TableExpression BuildTable(ITableDefinition definition)
		{
			var destType = definition.GetTableType();
			var tableName = definition.GetTableName();
			var columns = definition.GetColumns();
			var tableAlias = GetNextAlias();

			var param = Expression.Parameter(destType, "x");
			var assignList = columns
				.Select(col =>
					Expression.Bind(
						col.MemberInfo,
						CompleteExpression.Column(
							Expression.MakeMemberAccess(param, col.MemberInfo),
							col.ColumnName)))
				.ToList();

			var projection =
				Expression.Lambda(
					Expression.MemberInit(
						Expression.New(destType),
						assignList),
					param);

			var table = CompleteExpression.Table(destType, tableAlias, tableName, projection);
			return table;
		}

		private bool BuildRelationships(MemberExpression m)
		{
			// have we done any work?
			var flag = false;

			// see if we can find parent variable
			var from = fromExpressions.Where(f => f.FindObject(m.Expression) != null).SingleOrDefault();
			if (from == null)
			{
				// is there possibly more than one link in the chain? if not, we're done
				if (m.Expression.NodeType != ExpressionType.MemberAccess)
					return false;

				// if we couldn't build a relationship based on next link in chain, we're done
				if (!BuildRelationships(m.Expression as MemberExpression))
					return false;

				// we possibly did something for next link in chain, so let's acknowledge work has been done
				flag = true;

				// now that we've built a new relationship(s), try again?
				from = fromExpressions.Where(f => f.FindObject(m.Expression) != null).SingleOrDefault();

				// if still can't find parent variable, we're done
				if (from == null) return false;
			}

			// good so far! we have parent variable

			// are we even a complex type? if not, then we're done
			// we need to tell caller whether work has been done
			if (!m.Type.IsComplexType())
				return flag;

			// is there a relationship to the member variable?
			var association = mapper.GetRelationship(m.Member);

			// no relationship? then generate error: we don't know what to do next here
			if (association == null)
				throw new InvalidOperationException(string.Format("No assocation generated for '{0}'", m));

			// build table, associated from, and establish join expression columns
			var table = BuildTable(association.TableDefinition);
			var newFrom = CompleteExpression.From(table);
			var predicate = VisitSourceReferencedProjection(newFrom, association.JoinExpression);

			// are we dealing with a single row or multiple rows?
			if (typeof(System.Collections.IEnumerable).IsAssignableFrom(m.Type))
			{
				// we have multiple rows, so create new select statement to be returned, 
				// theoretically to be used in outer apply
				var select = CompleteExpression.Select(
					GetNextAlias(),
					ColumnReplacer.ReplaceColumns(table.Projection) as LambdaExpression,
					newFrom,
					where: CompleteExpression.Where(predicate.Body),
					orderBy: null,
					isProjection: false);
				from.From.AddAssignment(new AssignmentInformation()
				{
					Declaration = select,
					MembershipPath = m,
				});
			}
			else
			{
				// we have single object, so do the join behind the scenes so the column can be found

				// do the join ourselves and force it into the from object 
				// this way the associated select, wheres, etc don't have to be rewritten
				// should work because from is abstraction layer looking for a sourceexpression
				// (select, table, join, etc)
				var join = CompleteExpression.Join(
					from.From, table, JoinType.InnerJoin, predicate.Body);
				join.AddAssignment(new AssignmentInformation()
				{
					Declaration = table.Projection,
					MembershipPath = m,
				});
				from.UpdateSource(join);
			}

			return true;
		}

		protected LambdaExpression VisitSourceReferencedProjection(FromExpression from, Expression expression)
		{
			fromExpressions.Push(from);
			var projection = this.Visit(PartialEvaluator.Eval(expression.StripQuotes())) as LambdaExpression;
			fromExpressions.Pop();
			return projection;
		}

		protected virtual SelectExpression VisitSource(Expression e)
		{
			var select = this.Visit(e) as SelectExpression;
			if (select == null)
				throw new InvalidOperationException("There was an error binding this expression. The source of the where clause is not a select statement");

			if (select.IsProjection)
				select = BuildNestedSelect(select);

			return select;
		}

		private SelectExpression BuildNestedSelect(SelectExpression select)
		{
			var orderBy = BuildNestedOrderBy(select);

			var from = CompleteExpression.From(select);
			var projection = ColumnReplacer.ReplaceColumns(select.Projection) as LambdaExpression;
			select = CompleteExpression.Select(
				GetNextAlias(),
				projection,
				from,
				orderBy,
				isProjection: false);
			return select;
		}

		private OrderByClause BuildNestedOrderBy(SelectExpression select)
		{
			return BuildNestedOrderBy(select, select.OrderBy);
		}

		private OrderByClause BuildNestedOrderBy(SelectExpression select, OrderByClause orderBy)
		{
			if (orderBy == null)
				return null;

			var newOrderBy = new OrderByClause(BuildNestedOrderBy(select, orderBy.SecondarySortingClause));
			var columns = orderBy.SortColumns;
			var unmatchedColumns = new List<Expression>();
			foreach (var col in columns)
			{
				Expression expr = select.TryFindColumn(col.Item1);
				if (expr == null)
					expr = select.AddUnmappedColumn(col.Item1);
				newOrderBy.AppendColumn(Tuple.Create(expr, col.Item2));
			}
			return newOrderBy;
		}

		private IEnumerable<Expression> GetDistinctColumns(SelectExpression select, Expression expression)
		{
			var destType = expression.Type;
			if (!destType.IsComplexType())
				return new Expression[] { expression };

			var columns = DistinctColumnExtractor.GetDistinctColumns(mapper, expression.StripQuotes());

			fromExpressions.Push(select.From);
			columns = columns.Select(c => this.Visit(c)).ToList();
			fromExpressions.Pop();

			return columns;
		}
		#endregion

		#region Method Functions
		protected virtual Expression VisitSelectMethod(MethodCallExpression m)
		{
			var select = this.Visit(m.Arguments[0]) as SelectExpression;

			if (select.IsProjection)
			{
				var from = CompleteExpression.From(select);
				var projection = VisitSelectProjection(from, m.Arguments[1]);
				var orderBy = BuildNestedOrderBy(select);

				return CompleteExpression.Select(GetNextAlias(), projection, from, orderBy);
			}
			else
			{
				var projection = VisitSelectProjection(select.From, m.Arguments[1]);

				return CompleteExpression.Select(
					select.Alias,
					projection,
					select.From,
					select.OrderBy,
					select.Where,
					isProjection: true);
			}
		}

		protected LambdaExpression VisitSelectProjection(FromExpression from, Expression expr)
		{
			var projection = VisitSourceReferencedProjection(from, expr);
			if (!projection.ReturnType.IsComplexType())
			{
				var body = FixUpBooleanValue(projection.Body);
				var column = body as ColumnExpression;
				var name = column != null ? column.Name : string.Format("x{0:hhMMssffffff}", DateTime.UtcNow);
				projection = Expression.Lambda(
					CompleteExpression.Column(
						body,
						name),
					projection.Parameters.ToArray());
			}

			return projection;
		}

		protected virtual Expression VisitWhereMethod(MethodCallExpression m)
		{
			var select = VisitSource(m.Arguments[0]);
			var predicate = VisitSourceReferencedProjection(select.From, m.Arguments[1]);

			select.Where = CompleteExpression.Where(predicate.Body, select.Where);
			return select;
		}

		protected virtual Expression VisitSelectManyMethod(MethodCallExpression m)
		{
			var select = VisitSource(m.Arguments[0]);
			var selectSource = select.From.From as SourceExpression;

			var collection = VisitSourceReferencedProjection(select.From, m.Arguments[1]);
			var apply = collection.Body as SelectExpression;

			var joinType = apply.DefaultIfEmpty ? JoinType.OuterApply : JoinType.CrossApply;
			apply.DefaultIfEmpty = true;

			var join = CompleteExpression.Join(selectSource, apply, joinType);
			var from = CompleteExpression.From(join);

			var orderBy = select.OrderBy;
			var rightOrder = BuildNestedOrderBy(apply);
			if (orderBy != null)
				orderBy.InjectSecondarySortingClause(rightOrder);
			else if (rightOrder != null)
				orderBy = new OrderByClause(rightOrder);

			var projection =
				m.Arguments.Count() > 2 ?
					VisitSourceReferencedProjection(from, m.Arguments[2]) :
					ColumnReplacer.ReplaceColumns(apply.Projection) as LambdaExpression;

			return CompleteExpression.Select(
				GetNextAlias(),
				projection,
				from,
				orderBy: orderBy,
				where: select.Where,
				isProjection: true);
		}

		protected virtual Expression VisitDefaultIfEmptyMethod(MethodCallExpression m)
		{
			var select = this.Visit(m.Arguments[0]) as SelectExpression;
			select.DefaultIfEmpty = true;
			return select;
		}

		protected virtual Expression VisitJoinMethod(MethodCallExpression m)
		{
			if (m.Arguments.Count > 5)
				throw new NotSupportedException("Don't know how to support IEqualityComparer<TKey> parameter");

			var left = VisitSource(m.Arguments[0]);
			var right = VisitSource(m.Arguments[1]);

			var leftKey = VisitSourceReferencedProjection(left.From, m.Arguments[2]);
			var rightKey = VisitSourceReferencedProjection(right.From, m.Arguments[3]);

			var where =
				left.Where != null ?
					CompleteExpression.Where(left.Where.Predicate, right.Where) :
					right.Where;

			var orderBy = OrderByClause.CombineOrderByClauses(left.OrderBy, right.OrderBy);

			var leftSource = left.From.From as SourceExpression;
			var rightSource = right.From.From as SourceExpression;

			JoinType joinType;
			if (left.DefaultIfEmpty)
			{
				if (right.DefaultIfEmpty)
					joinType = JoinType.FullOuterJoin;
				else
					joinType = JoinType.RightOuterJoin;
			}
			else
			{
				if (right.DefaultIfEmpty)
					joinType = JoinType.LeftOuterJoin;
				else
					joinType = JoinType.InnerJoin;
			}
			left.DefaultIfEmpty = false;
			right.DefaultIfEmpty = false;

			var join = CompleteExpression.Join(leftSource, rightSource, joinType, Expression.Equal(leftKey.Body, rightKey.Body));
			var from = CompleteExpression.From(join);

			var projection = VisitSourceReferencedProjection(from, m.Arguments[4]);
			return CompleteExpression.Select(
				GetNextAlias(),
				projection,
				from,
				where: where,
				orderBy: orderBy,
				isProjection: true);
		}

		private Expression FixUpBooleanValue(Expression expr)
		{
			if (expr.Type != typeof(bool)) return expr;

			return
				Expression.Condition(
					expr,
					Expression.Constant(true, typeof(bool)),
					Expression.Constant(false, typeof(bool)));
		}

		protected virtual Expression VisitTakeMethod(MethodCallExpression m)
		{
			var select = this.Visit(m.Arguments[0]) as SelectExpression;
			var take = this.Visit(m.Arguments[1]);

			if (!typeof(int).IsAssignableFrom(take.Type))
				throw new InvalidOperationException("The parameter of the Take() function call must return an int.");

			take = FixUpBooleanValue(take);

			if (select.Take != null)
				select = BuildNestedSelect(select);

			return CompleteExpression.Select(
				select.Alias,
				select.Projection,
				select.From,
				select.OrderBy,
				select.Where,
				take: take,
				isProjection: true);
		}

		protected virtual Expression VisitOrderByMethod(MethodCallExpression m)
		{
			var select = this.Visit(m.Arguments[0]) as SelectExpression;
			if (select.Take != null)
				select = BuildNestedSelect(select);

			var sortDirection = m.Method.Name.Contains("Desc") ? OrderByClause.SortDirection.Descending : OrderByClause.SortDirection.Ascending;

			var sortColumns = GetDistinctColumns(select, m.Arguments[1]);

			if (select.OrderBy == null)
				select.OrderBy = new OrderByClause(null);

			select.OrderBy.ClearColumns();
			foreach (var col in sortColumns)
				select.OrderBy.AppendColumn(Tuple.Create(FixUpBooleanValue(col), sortDirection));

			return select;
		}

		protected virtual Expression VisitThenByMethod(MethodCallExpression m)
		{
			var select = this.Visit(m.Arguments[0]) as SelectExpression;
			if (select.OrderBy.SortColumns.Count == 0)
				throw new InvalidOperationException("ThenBy() cannot be called without first calling OrderBy()");

			var predicate = this.VisitSourceReferencedProjection(select.From, m.Arguments[1]);
			var sortDirection = m.Method.Name.Contains("Desc") ? OrderByClause.SortDirection.Descending : OrderByClause.SortDirection.Ascending;

			var sortColumns = GetDistinctColumns(select, predicate.Body);

			foreach (var col in sortColumns)
				select.OrderBy.AppendColumn(Tuple.Create(FixUpBooleanValue(col), sortDirection));

			return select;
		}
		#endregion

		public static Expression Bind(Expression expr, INameMapper mapper)
		{
			var qt = new QueryBinder(mapper);
			expr = qt.Visit(expr);
			return expr;
		}
	}
}
